package org.redmagic.dao;

import java.util.List;
import java.util.Set;

import org.bson.types.ObjectId;
import org.redmagic.api.PropertyDao;
import org.redmagic.domain.InternalServiceProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;

@Repository
public class MongoPropertyDao implements PropertyDao {

	private static final Logger LOGGER = LoggerFactory.getLogger(MongoPropertyDao.class);
	private static final String DATABASE_NAME = "magic";
	private static final String PROPERTY_COLLECTION_NAME = "properties";
	private static final String TAGS_FIELD = "tags";
	
	private final DBCollection properties;
	
	@Autowired
	public MongoPropertyDao(MongoClient mongoClient) {
		this.properties = mongoClient.getDB(DATABASE_NAME).getCollection(PROPERTY_COLLECTION_NAME);
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public Set<String> getAllTags() {
		return Sets.newHashSet(properties.distinct(TAGS_FIELD));
	}
	
	@Override
	public Set<String> getSelectedTags(String key) {
		Set<String> tags = Sets.newHashSet();
		DBObject tagsInDatabaseObject = properties.findOne(new BasicDBObject("_id", new ObjectId(key)), new BasicDBObject("tags", "1"));
		BasicDBList tagsInDatabase = (BasicDBList) tagsInDatabaseObject.get("tags");
		for (Object tag : tagsInDatabase) {
			tags.add((String) tag);
		}
		return tags;
	}
	
	@Override
	public long getPropertyCount() {
		return properties.count();
	}
	
	@Override
	public List<InternalServiceProperty> getAllProperties() {
		LOGGER.info("Getting all properties from the database");
		List<InternalServiceProperty> foundProperties = Lists.newArrayList();
		DBCursor resultCursor = properties.find();
		
		while (resultCursor.hasNext()) {
			foundProperties.add(getMagicProperty(resultCursor.next()));
		}
		LOGGER.info("Found {} properties", foundProperties.size());
		return foundProperties;
	}

	@Override
	public Optional<InternalServiceProperty> findProperty(String key, Set<String> tags, String group) {		
		DBObject query = prepareQuery(key, tags, group);
		LOGGER.debug("Executing query: {}", query);
		
		DBCursor result = properties.find(query);
		int resultCount = result.count();
		
		switch (resultCount) {
		case 1:
			return Optional.fromNullable(getMagicProperty(result.next()));
		case 2:
			return getPropertyPrivatePreferred(result);
		default:
			return resultNotFound(resultCount);
		}
	}
	
	private BasicDBObject prepareQuery(String key, Set<String> tags, String group) {
		BasicDBList allTags = new BasicDBList();
		for (String tag : tags) {
			allTags.add(tag);
		}
		
		return new BasicDBObject()
			.append("name", key)
			.append("tags", new BasicDBObject()
					.append("$all", allTags)
					.append("$size", tags.size()))
			.append("group", group);
	}
	
	private Optional<InternalServiceProperty> getPropertyPrivatePreferred(DBCursor results) {
		DBObject firstResult = results.next();
		if (isPublicProperty(firstResult)) {
			DBObject secondResult = results.next();
			return Optional.fromNullable(getMagicProperty(secondResult));
		} else {
			return Optional.fromNullable(getMagicProperty(firstResult));
		}
	}
	
	private boolean isPublicProperty(DBObject property) {
		return property.get("group").equals("Public");
	}
	
	private Optional<InternalServiceProperty> resultNotFound(int resultCount) {
		if (resultCount == 0) {
			LOGGER.debug("No results found.");
			return Optional.absent();
		} else {
			// Should never get here
			LOGGER.error("Found {} results, possible query problem or duplicate property in database");
			throw new RuntimeException("Found " + resultCount + " results, expected 1");
		}
	}
	
	private InternalServiceProperty getMagicProperty(DBObject property) {
		ObjectId id = (ObjectId) property.get("_id");
		String key = id.toString();
		String name = (String) property.get("name");
		String value = (String) property.get("value");
		String encoding = "UTF-8";
		String group = (String) property.get("group");
		Set<String> tags = Sets.newHashSet();
		BasicDBList jsonTags = (BasicDBList) property.get("tags");
		for (int i = 0; i < jsonTags.size(); i++) {
			tags.add((String) jsonTags.get(i));
		}
		
		return new InternalServiceProperty(key, name, value, encoding, tags, group);
	}
	
	@Override
	public void createProperty(String name, Set<String> tags, String value, String group) {
		LOGGER.info("Inserting property: Name: {}", name);
		
		BasicDBList tagsList = new BasicDBList();
		for (String tag : tags) {
			tagsList.add(tag);
		}
		
		DBObject property = BasicDBObjectBuilder.start()
				.add("_id", new ObjectId())
				.add("name", name)
				.add("tags", tagsList)
				.add("value", value)
				.add("group", group)
				.get();
		
		properties.insert(property);
	}
	
	@Override
	public void updateProperty(String key, String name, Set<String> tags, String value, String group) {
		DBObject query = new BasicDBObject("_id", new ObjectId(key));
		
		BasicDBList tagsList = new BasicDBList();
		for (String tag : tags) {
			tagsList.add(tag);
		}
		
		DBObject update = BasicDBObjectBuilder.start()
				.add("name", name)
				.add("tags", tagsList)
				.add("value", value)
				.add("group", group)
				.get();
		
		properties.update(query, update);
	}

	@Override
	public void removeProperty(String key) {
		DBObject propertyToRemove = new BasicDBObject("_id", new ObjectId(key));
		properties.remove(propertyToRemove);
	}

}
